package com.fourforfo.fourmall.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.fourforfo.fourmall.product.service.CategoryBrandRelationService;
import com.fourforfo.fourmall.product.vo.Catalog2VO;
import org.apache.commons.lang.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fourforfo.common.utils.PageUtils;
import com.fourforfo.common.utils.Query;

import com.fourforfo.fourmall.product.dao.CategoryDao;
import com.fourforfo.fourmall.product.entity.CategoryEntity;
import com.fourforfo.fourmall.product.service.CategoryService;


@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

    @Autowired
    CategoryBrandRelationService categoryBrandRelationService ;

    @Autowired
    StringRedisTemplate redisTemplate;

    @Autowired
    RedissonClient redissonClient;
    /**
     * 以树形结构返回所有Category
     * @return
     */
    @Override
    public List<CategoryEntity> listWithTree(){
        //1.查出所有分类
        List<CategoryEntity> entities = baseMapper.selectList(null);

        //2.组装成树形结构
        //一级分类集合
        List<CategoryEntity> leve1Menus = entities.stream()
                .filter(categoryEntity ->categoryEntity.getParentCid()==0)
                .map((menu)->{
                    menu.setChildren(getChildrens(menu,entities));
                    return menu;
                }).sorted((menu1,menu2)->{
                    return (menu1.getSort()==null?0:menu1.getSort())-(menu2.getSort()==null?0:menu2.getSort());
                }).collect(Collectors.toList());
        return leve1Menus;
    }

    @Override
    public void removeMenuByIds(List<Long> asList) {
        //TODO 增加删除条件

        baseMapper.deleteBatchIds(asList);
    }

    //递归查找所有菜单的子菜单
    private List<CategoryEntity> getChildrens(CategoryEntity root,List<CategoryEntity> menus){
        return menus.stream().filter(categoryEntity -> {
            return categoryEntity.getParentCid() == root.getCatId();
        }).map(categoryEntity -> {
            //找到子菜单，
            categoryEntity.setChildren(getChildrens(categoryEntity,menus));
            return categoryEntity;
        }).sorted((menu1,menu2)->{
            return (menu1.getSort()==null?0:menu1.getSort())-(menu2.getSort()==null?0:menu2.getSort());
        }).collect(Collectors.toList());
    }


    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }

    /**
     * 使用CacheEvict实现失效模式
     * @param category
     */
    @Caching(evict = {
            @CacheEvict(value={"category"},key="'getLevel1'"),
            @CacheEvict(value={"category"},key="'getCatalogJson'")
    })
    @Override
    public void updateCascade(CategoryEntity category) {
        this.updateById(category);
        if(!StringUtils.isEmpty(category.getName())) {
            categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
        }
    }

    @Override
    public Long[] getCatelogPath(Long catelogId) {
        List<CategoryEntity> categoryEntities = baseMapper.getCatelogPathById(catelogId);
        int size = categoryEntities.size();
        Long[] catelogPath = new Long[size];
        for(int i=0;i<size;i++){
            catelogPath[i] = categoryEntities.get(size-i-1).getCatId();
        }
        return catelogPath;
    }

    /**
     * 查出一级分类
     * @return
     */
    @Cacheable(value={"category"},key="#root.method.name")
    @Override
    public List<CategoryEntity> getLevel1() {
        System.out.println("查询一级菜单");
        return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("cat_level",1));
    }


    /**
     * 封装分类数据
     *  TODO Redis exception; nested exception is io.lettuce.core.RedisException: io.netty.util.internal.OutOfDirectMemoryError: failed to allocate 46137344 byte(s) of direct memory (used: 58720256, max: 100663296)
     *  异常原因：
     *  1）springboot2.0以后默认使用lettuce作为操作redis的客户端，它使用netty进行通信
     *  2）lettuce的bug导致netty堆外内存溢出
     * @return
     */
    @Cacheable(value = "category",key = "#root.method.name")
    @Override
    public Map<String ,List<Catalog2VO>> getCatalogJson(){

        System.out.println("查询了数据。。。");

        //1.查出所有分类
        List<CategoryEntity> entities = baseMapper.selectList(null);

        //2.组装成前端能识别的数据结构
        Map<String, List<Catalog2VO>> map = entities.stream()
                .filter(categoryEntity -> categoryEntity.getParentCid() == 0)//获取一级菜单
                .collect(Collectors.toMap(
                        catalog1 -> String.valueOf(catalog1.getCatId()),//设置key
                        catalog1 -> getCatalog2VOs(catalog1, entities)//设置List<Catalog2VO>格式的value
                ));

        return map;
    }

    /**
     * 未使用@Cacheable注解的方法
     * 获取三级菜单数据
     * @return
     */
    public Map<String, List<Catalog2VO>> getCatalogJson2() {


        //加入Redis缓存,注意缓存中村的数据都是Json字符串
        //json跨语言，跨平台兼容

        /**
         * 1. 空结果缓存：防止缓存穿透
         * 2. 设置过期时间（加随机值）：防止缓存雪崩
         * 3. 加锁：解决缓存击穿
         */

        ValueOperations<String, String> operations = redisTemplate.opsForValue();
        String catalogJson = operations.get("catalogJson");
        Map<String, List<Catalog2VO>> catalogJsonFromDB;
        if(StringUtils.isEmpty(catalogJson)){
            //缓存中没有数据
//            System.out.println("即将查询数据库。。。");

            catalogJsonFromDB = getCatalogJsonFromDbWithRedissonLock();
        }else{
//            System.out.println("缓存命中。。。");
            catalogJsonFromDB = JSON.parseObject(catalogJson,new TypeReference<Map<String,List<Catalog2VO>>>(){});
        }
//        Map<String, List<Catalog2VO>> catalogJsonFromDB = getCatalogJsonFromDB();

        return catalogJsonFromDB;

    }


    /**
     * redisson实现分布式锁
     * 缓存数据一致性问题
     * 1）双写模式
     * 2）失效模式
     *
     */
    public Map<String, List<Catalog2VO>> getCatalogJsonFromDbWithRedissonLock() {

        //1.占分布式锁。去Redis占坑
        RLock rLock = redissonClient.getLock("catalogJson_redisson");
        try{
            rLock.lock();
            Map<String, List<Catalog2VO>> dataFromDb = getDataFromDb();
            return dataFromDb;
        }catch (Exception e){
            log.error("加锁失败或者查询数据库失败{}",e);
        }finally {
            rLock.unlock();
        }
        return null;
    }

    /**
     * redis实现分布式锁
     *
     */
    public Map<String, List<Catalog2VO>> getCatalogJsonFromDbWithRedisLock() {

        //1.占分布式锁。去Redis占坑
        //1.1 setIfAbsent方法等同于SETNX命令
        //1.2 注意：加锁和设置过期时间必须是一个原子操作，在调用setIfAbsent传入锁名的同时必须传入过期时间
        String uuid = UUID.randomUUID().toString();
        Boolean isSet = redisTemplate.opsForValue().setIfAbsent("lock",uuid,300,TimeUnit.SECONDS);
        if(isSet){
            //加锁成功
            System.out.println("获取分布式锁成功...");
            Map<String, List<Catalog2VO>> dataFromDb;
            try{
                dataFromDb = getDataFromDb();
            }finally {
                /**
                 * 获取值对比+对比成功删除 这两步必需是原子操作，
                 * 不然还是存在获取到lockValue但还没删除时由于时间过期被其他进程拿到锁的可能
                 * 所以下面这段代码可以使用lua脚本来实现
                 */
//            String lockValue = redisTemplate.opsForValue().get("lock");
//            if(uuid.equals(lockValue)){
//                //删除我自己的锁
//                redisTemplate.delete("lock");//删除锁
//            }
                String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
                Long deleteFlag = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);

            }

            return dataFromDb;
        }else{
            //加锁失败..重试
            System.out.println("获取分布式锁失败...重试ing");
            try{
                Thread.sleep(200);
            }catch (Exception e){

            }
            //通过递归实现自旋锁
            return getCatalogJsonFromDbWithRedisLock();
        }


    }

    /**
     * 使用本地锁实现
     *
     */
    public Map<String, List<Catalog2VO>> getCatalogJsonFromDbWithLocalLock() {

        //由于Springboot所有组件在容器中都是单例的，可以使用synchronized (this)
        //TODO 本地锁，synchronized，JUC(Lock)，在分布式情况下无法锁住所有集群

        synchronized (this){
            return getDataFromDb();
        }

    }

    //从数据库中取分类数据
    private Map<String, List<Catalog2VO>> getDataFromDb() {
        String catalogJson = redisTemplate.opsForValue().get("catalogJson");
        if (!StringUtils.isEmpty(catalogJson)) {
            return JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2VO>>>() {
            });
        }
        System.out.println("查询了数据量。。。");

        //1.查出所有分类
        List<CategoryEntity> entities = baseMapper.selectList(null);

        //2.组装成前端能识别的数据结构
        Map<String, List<Catalog2VO>> map = entities.stream()
                .filter(categoryEntity -> categoryEntity.getParentCid() == 0)//获取一级菜单
                .collect(Collectors.toMap(
                        catalog1 -> String.valueOf(catalog1.getCatId()),//设置key
                        catalog1 -> getCatalog2VOs(catalog1, entities)//设置List<Catalog2VO>格式的value
                ));

        String result = JSON.toJSONString(map);
        redisTemplate.opsForValue().set("catalogJson", result, 100 + (long) (Math.random() * 50), TimeUnit.MINUTES);
        return map;
    }


    /**
     * 从所有商品分类中找到二级菜单，然后在从中找到三级菜单并封装到二级菜单
     * 逻辑比较简单，就写在一个方法里了，看不懂的慢慢看吧
     * @param catalog1
     * @param categories
     * @return
     */
    private List<Catalog2VO> getCatalog2VOs(CategoryEntity catalog1,List<CategoryEntity> categories){

        List<Catalog2VO> catalog2VOS = categories.stream().filter(categoryEntity ->
            categoryEntity.getParentCid() == catalog1.getCatId() //获取二级分类
        ).sorted(
                Comparator.comparingInt(category -> (category.getSort() == null ? 0 : category.getSort()))
        ).map(categoryLevel2 -> { //组成前端能识别的二级分类格式
            Catalog2VO catalog2VO = new Catalog2VO();
            catalog2VO.setCatalog1Id(catalog1.getCatId());
            catalog2VO.setId(categoryLevel2.getCatId());
            catalog2VO.setName(categoryLevel2.getName());

            //获取当前二级分类的三级分类集合
            List<Catalog2VO.Catalog3VO> catalog3VOS = categories.stream().filter(categoryEntity ->
                    categoryEntity.getParentCid() == categoryLevel2.getCatId()
            ).sorted(
                    Comparator.comparingInt(category -> (category.getSort() == null ? 0 : category.getSort()))
            ).map(categoryLevel3->{
                Catalog2VO.Catalog3VO catalog3VO = new Catalog2VO.Catalog3VO();
                catalog3VO.setId(categoryLevel3.getCatId());
                catalog3VO.setName(categoryLevel3.getName());
                catalog3VO.setCatalog2Id(categoryLevel2.getCatId());
                return catalog3VO;
            }).collect(Collectors.toList());
            catalog2VO.setCatalog3List(catalog3VOS);

            return catalog2VO;
        }).collect(Collectors.toList());

        return catalog2VOS;

    }



}